Memory Management
Introduction
Memory management in C is divided into two parts. One part is system managed and the other part is manually managed by the user.
The system-managed memory is mainly the variables inside functions (local variables). These variables are entered into memory when the function is run and are automatically unloaded from memory when the function is finished. The area where these variables are stored is called the "stack", and the memory where the "stack" is located is managed automatically by the system.
The memory that is managed manually by the user is mainly the variables that are present throughout the program (global variables) and need to be released from memory manually by the user. If you forget to release a variable after it has been used, it will continue to occupy memory until the program exits, a situation known as a "memory leak". The memory in which these variables are located is called the "heap" and the memory in which the "heap" is located is managed manually by the user.
void pointer
As mentioned in the previous sections, each block of memory has an address and a pointer variable can be used to access the block of memory at the specified address. Pointer variables must have a type, otherwise the compiler would not know how to interpret the binary data held in the memory block. However, when requesting memory from the system, there is sometimes uncertainty about what data will be written to memory, and it is necessary to obtain the memory block first and later determine the type of data to be written.
To meet this need, C provides an indefinite type of pointer called a void pointer. It only has information about the address of the block of memory, not the type, and when the block is used, the compiler is given an additional indication of what type of data is inside.
On the other hand, a void pointer is equivalent to an untyped pointer, and can point to data of any type, but cannot interpret it. void pointers are interconvertible with all other types of pointers; a pointer of any type can be converted to a void pointer, and a void pointer can be converted to a pointer of any type.
int x = 10;
void* p = &x; // integer pointer to void pointer
int* q = p; // void pointer to integer pointer
The above example demonstrates how integer pointers and void pointers can be converted to each other. &x
is an integer pointer, p
is a void pointer, and the address of &x
is automatically interpreted as a void type when it is assigned. Similarly, when p
is then assigned to the integer pointer q
, the address of p
is automatically interpreted as an integer pointer.
Note that since it is not known what type of value the void pointer points to, you cannot use the *
operator to retrieve the value it points to.
char a = 'X';
void* p = &a;
printf("%c\n", *p); // report error
In the example above, p
is a void pointer, so you can't use *p
to get out the value pointed to by the pointer at this point.
The important thing about the void pointer is that many memory-related functions return a void pointer, giving only the address information of the memory block, so it is introduced at the top.
malloc()
The malloc()
function is used to allocate memory. It asks the system for a block of memory, and the system allocates a contiguous block of memory to it in the heap'. Its prototype is defined in the header file
stdlib.h`.
void* malloc(size_t size)
It accepts a non-negative integer as an argument, indicating the number of bytes of memory to be allocated, and returns a void pointer to the allocated memory block. This makes sense, because the malloc()
function does not know what type of data will be stored in that block of memory, so it can only return an untyped void pointer.
It is possible to use malloc()` to allocate memory for any type of data, and it is common to use the
sizeof() function to work out the length in bytes required for a particular data type, and then pass this length to ``malloc()
.
int* p = malloc(sizeof(int));
*p = 12;
printf("%d\n", *p); // 12
In the above example, a section of memory is allocated for the integer type and then the integer 12
is placed inside this section. There is no need to use malloc()
for this example, as C automatically provides memory for integers (in this case 12
).
Sometimes, to make the code more readable, you can do a forced type conversion on the pointer returned by malloc()
.
int* p = (int*) malloc(sizeof(int));
The above code takes the void pointer returned by malloc()
and forces it into an integer pointer.
Since the argument to sizeof()
can be a variable, the above example could also be written as follows.
int* p = (int*) malloc(sizeof(*p));
`malloc()
allocates memory at the risk of failing to allocate it, which returns the constant NULL. the value of Null is 0, which is a memory address that cannot be read or written, and can be interpreted as a pointer to nowhere. It is defined in several header files, including stdlib.h
, so NULL
can be used whenever malloc()
can be used. Since there is a possibility of allocation failure, it is a good idea to check after using malloc()
that the allocation was successful.
int* p = malloc(sizeof(int));
if (p == NULL) {
// memory allocation failed
}
// or
if (!p) {
//...
}
The above example determines whether malloc()
has been allocated successfully by determining whether the returned pointer p
is NULL
.
The most common use of malloc()
is to allocate memory for arrays and custom data structures.
int* p = (int*) malloc(sizeof(int) * 10);
for (int i = 0; i < 10; i++)
p[i] = i * 5;
In the above example, p
is an integer pointer to a section of memory that can hold 10 integers, so it can be used as an array.
One advantage of malloc()
, which is used to create arrays, is that it can create dynamic arrays, i.e. arrays of different lengths depending on the number of members.
int* p = (int*) malloc(n * sizeof(int));
In the above example, malloc()
can dynamically allocate different sizes to the array depending on the variable n
.
Note that malloc()
does not initialize the allocated memory, which still holds the original values. If this memory is used without initialisation, the previous values may be read from it. The programmer is responsible for the initialization himself. For example, string initialization can be done using the strcpy()
function.
char* p = malloc(4);
strcpy(p, "abc");
// or
p = "abc";
In the above example, the character pointer p
points to a 4-byte section of memory. strcpy()
initializes this section of memory by copying the string "abc" into it.
free()
free()
is used to free the memory allocated by the malloc()
function, returning the memory to the system for reuse, otherwise the block will remain occupied until the end of the program. The prototype of this function is defined in the header file stdlib.h
.
void free(void* block)
The argument to free()
in the above code is the memory address returned by malloc()
. Here is an example of usage.
int* p = (int*) malloc(sizeof(int));
*p = 12;
free(p);
Note that once an allocated block of memory has been freed, you should not manipulate the freed address again, nor should you use free()
again to free the address a second time.
A very common mistake is to allocate memory inside a function, but not use free()
to free it at the end of the function call.
void gobble(double arr[], int n) {
double* temp = (double*) malloc(n * sizeof(double));
// ...
}
In the example above, the function gobble()
allocates memory internally, but does not write free(temp)
. This causes the block of memory occupied to remain after the function has finished running, and if gobble()
is called multiple times, multiple blocks of memory will be left behind. And, since the pointer temp
has disappeared, there is no way to access these memory blocks and use them again.
calloc()
The calloc()
function does something similar to malloc()
in that it also allocates blocks of memory. The prototype of this function is defined in the header file stdlib.h
.
There are two main differences between the two.
(1) calloc()
accepts two arguments, the first being the number of values of a certain data type, and the second being the unit byte length of that data type.
void* calloc(size_t n, size_t size);
The return value of `calloc()
is also a void pointer. On allocation failure, NULL is returned.
(2) calloc()
will initialize all the allocated memory to 0
. malloc()
does not initialize the memory, and an additional call to the memset()
function is made if you want to initialize to 0
.
int* p = calloc(10, sizeof(int));
// equivalent to
int* p = malloc(sizeof(int) * 10);
memset(p, 0, sizeof(int) * 10);
In the above example, calloc()
is equivalent to malloc() + memset()
.
The block of memory allocated by calloc()
is also freed using free()
.
realloc()
The realloc()
function is used to modify the size of an allocated block of memory, either by scaling it up or down, returning a pointer to the new block of memory. The prototype of this function is defined in the header file stdlib.h
.
void* realloc(void* block, size_t size)
It accepts two arguments.
block
: a pointer to an already allocated block of memory (generated bymalloc()
orcalloc()
orrealloc()
).size
: the new size of this memory block in bytes.
realloc()
may return a completely new address (the data is also automatically copied over), or it may return the same address as the original. realloc()
gives preference to scaling down on the original block of memory and not moving data around as much as possible, so the original address is usually returned. If the new memory block is smaller than the original size, the excess is discarded; if it is larger than the original size, the addition is not initialised (the programmer can automatically call memset()
).
Here is an example where b
is an array pointer and realloc()
dynamically resizes it.
int* b;
b = malloc(sizeof(int) * 10);
b = realloc(b, sizeof(int) * 2000);
In the example above, the pointer b
originally pointed to a 10 member integer array, which was adjusted to a 2000 member array using realloc()
. This is the benefit of manually allocating the array memory, allowing the length of the array to be adjusted at runtime at any time.
The first argument to realloc()
can be NULL, which is then equivalent to creating a new pointer.
char* p = realloc(NULL, 3490);
// equivalent to
char* p = malloc(3490);
If the second argument to realloc()
is 0
, the memory block will be freed.
Because of the possibility of allocation failure, it is a good idea to check that the return value of realloc()
is NULL after calling it. the data in the original block will not be changed in case of allocation failure.
float* new_p = realloc(p, sizeof(*p * 40));
if (new_p == NULL) {
printf("Error reallocing\n");
return 1;
}
Note that realloc()
does not initialize the memory block.
restrict descriptor
When declaring a pointer variable, you can use the restrict
descriptor to tell the compiler that the block of memory is only accessible by the current pointer and that other pointers cannot read or write to the block of memory. Such pointers are called restrict pointer
s.
int* restrict p;
p = malloc(sizeof(int));
In the above example, the pointer variable p
is declared with the restrict
specifier, making p
a restricted pointer. Later, when p
points to a piece of memory returned by the malloc()
function, it means that the area is only accessible through p
and no other access exists.
int* restrict p;
p = malloc(sizeof(int));
int* q = p;
*q = 0; // undefined behaviour
In the above example, another pointer q
points to the same block of memory as the restricted pointer p
, which now has both p
and q
accesses. This violates the promise to the compiler that a later assignment to that memory region via *q
will result in undefined behaviour.
memcpy()
memcpy()
is used to copy a piece of memory to another piece of memory. The prototype of this function is defined in the header file string.h
.
void* memcpy(
void* restrict dest,
void* restrict source,
size_t n
);
In the above code, dest
is the destination address, source
is the source address, and the third argument n
is the number of bytes to be copied n
. If 10 array members of type double were to be copied, n
would be equal to 10 * sizeof(double)
, not 10
. This function will copy n
bytes, starting from source
, to dest
.
Both dest
and source
are void pointers, indicating that there is no restriction on the type of pointer here, and that all types of memory data can be copied. Both have the restrict keyword, indicating that the two memory blocks should not have areas that overlap each other.
The return value of memcpy()
is the first argument, which is a pointer to the target address.
Since memcpy()
just copies the value of one section of memory, to another section of memory, there is no need to know what type of data is inside the memory. Here is an example of copying a string.
#include <stdio.h>
#include <string.h>
int main(void) {
char s[] = "Goats!";
char t[100];
memcpy(t, s, sizeof(s)); // copy 7 bytes, including terminator
printf("%s\n", t); // "Goats!"
return 0;
}
In the above example, the memory where the string s
is located is copied to the memory where the character array t
is located.
memcpy()` can replace
strcpy() for string copying, and is a better method, not only safer but also faster, it does not check for the ``\0
character at the end of the string.
char* s = ``hello world``;
size_t len = strlen(s) + 1;
char *c = malloc(len);
if (c) {
// strcpy() is written as
strcpy(c, s);
// memcpy() is written
memcpy(c, s, len);
}
In the example above, the two writes have exactly the same effect, but memcpy()
is better than strcpy()
.
Using the void pointer, it is also possible to customise a function that copies memory.
void* my_memcpy(void* dest, void* src, int byte_count) {
char* s = src;
char* d = dest;
while (byte_count--) {
*d++ = *s++;
}
return dest;
}
In the above example, whatever type of pointers are passed in for dest
and src
, they are redefined as one-byte Char pointers so that they can be copied byte by byte. The *d++ = *s++
statement is equivalent to first executing *d = *s
(the value of the source byte is copied to the target byte) and then moving each to the next byte. Finally, the pointer to the copied dest
is returned for subsequent use.
memmove()
The memmove()
function is used to copy data from one section of memory to another. Its main difference from memcpy()
is that it allows the target area to overlap with the source area. If there is an overlap, the contents of the source area are changed; if there is no overlap, it behaves in the same way as memcpy()
.
The prototype of this function is defined in the header file string.h
.
void* memmove(
void* dest,
void* source,
size_t n
);
In the above code, dest
is the destination address, source
is the source address, and n
is the number of bytes to be moved. Both dest
and source
are void pointers, indicating that any type of memory data can be moved and that the two memory areas can have overlap.
The return value of memmove()
is the first argument, a pointer to the destination address.
int a[100];
// ...
memmove(&a[0], &a[1], 99 * sizeof(int));
In the above example, the 99 members of the array, starting with the array member a[1]
, are moved forward one position.
Here is another example.
char x[] = "Home Sweet Home";
// Output Sweet Home Home
printf("%s\n", (char *) memmove(x, &x[5], 10));
In the above example, the 10 bytes starting at position 5 of the string x
is "Sweet Home", and memmove()
moves it forward to position 0, so that x
becomes "Sweet Home Home ".
memcmp()
The memcmp()
function is used to compare two memory regions. Its prototype is defined in string.h
.
int memcmp(
const void* s1,
const void* s2,
size_t n
);
It accepts three arguments, the first two being pointers to be used for comparison and the third specifying the number of bytes to be compared.
Its return value is an integer. Each byte of the two memory areas is interpreted in character form and compared in dictionary order, returning 0
if they are the same, an integer greater than 0 if s1
is greater than s2
, and an integer less than 0 if s1
is less than s2
.
char* s1 = "abc";
char* s2 = "acd";
int r = memcmp(s1, s2, 3); // less than 0
The above example compares the first three bytes of s1
and s2
. Since s1
is smaller than s2
, r
is an integer less than 0, typically -1.
Here is another example.
char s1[] = {'b', 'i', 'g', '\0', 'c', 'a', 'r'};
char s2[] = {'b', 'i', 'g', '\0', 'c', 'a', 't'};
if (memcmp(s1, s2, 3) == 0) // true
if (memcmp(s1, s2, 4) == 0) // true
if (memcmp(s1, s2, 7) == 0) // false
The above example shows that memcmp()
can compare memory regions with the string terminator \0
inside.